package org.terramagnet.http;

import org.terramagnet.http.signature.Signer;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Map;

/**
 * HTTP连接工具——fluent API. Copied and partial modified from <a href="http://jsoup.org/">Jsoup</a>.
 * <p>
 * To get a new Connection, use {@link HttpFacade#connect(String)}. Connections contain {@link Connection.Request}
 * and {@link Connection.Response} objects. The request objects are reusable as prototype requests.
 * </p>
 * <p>
 * Request configuration can be made using either the shortcut methods in Connection (e.g. {@link #userAgent(String)}),
 * or by methods in the Connection.Request object directly. All request configuration must be made before the request is
 * executed.
 * </p>
 */
public interface Connection {

    /**
     * GET and POST http methods.
     */
    public enum Method {

        GET(false), POST(true), PUT(true), DELETE(false), PATCH(true);

        private final boolean hasBody;

        private Method(boolean hasBody) {
            this.hasBody = hasBody;
        }

        /**
         * Check if this HTTP method has/needs a request body
         *
         * @return if body needed
         */
        public final boolean hasBody() {
            return hasBody;
        }
    }

    /**
     * Set the request URL to fetch. The protocol must be HTTP or HTTPS.
     *
     * @param url URL to connect to
     * @return this Connection, for chaining
     */
    public Connection url(URL url);

    /**
     * Set the request URL to fetch. The protocol must be HTTP or HTTPS.
     *
     * @param url URL to connect to
     * @return this Connection, for chaining
     */
    public Connection url(String url);

    /**
     * 设置URL中的路径变量. 路径变量是以{@code {name}}的形式出现在URL中。 如：
     * <pre>{@code /{version}/accounts/{uid}/messages}</pre>
     *
     * @param name 变量名
     * @param value 变量值
     * @return this Connection, for chaining
     */
    public Connection var(String name, String value);

    /**
     * Set the request user-agent header.
     *
     * @param userAgent user-agent to use
     * @return this Connection, for chaining
     */
    public Connection userAgent(String userAgent);

    /**
     * Set the request timeouts (connect and read). If a connectTimeout occurs, an IOException will be thrown. The default
     * connectTimeout is 20 seconds (20000 millis) and readTimeout is 40 seconds (40000 millis). A timeout of zero is treated as an infinite timeout.
     *
     * @param millis number of milliseconds (thousandths of a second) before timing out connects or reads.
     * @return this Connection, for chaining
     */
    public Connection timeout(int millis);

    /**
     * Set the request timeouts (connect and read). If a connectTimeout occurs, an IOException will be thrown. The default
     * connectTimeout is 20 seconds (20000 millis) and readTimeout is 40 seconds (40000 millis). A timeout of zero is treated as an infinite timeout.
     *
     * @param connectTimeout number of milliseconds (thousandths of a second) before timing out connects.
     * @param readTimeout number of milliseconds (thousandths of a second) before timing out reads.
     * @return this Connection, for chaining
     */
    public Connection timeout(int connectTimeout, int readTimeout);

    /**
     * 设置最大重试次数. 连接超时的情况下重试请求的次数，{@code Read time out} 不会重试。
     *
     * @param maxRetryTimes 最大重试次数
     * @return this Connection, for chaining
     */
    public Connection retry(int maxRetryTimes);

    /**
     * Set the request method to use, GET or POST. Default is GET.
     *
     * @param method HTTP request method
     * @return this Connection, for chaining
     */
    public Connection method(Method method);

    /**
     * Configures the connection to not throw exceptions when a HTTP error occurs. (4xx - 5xx, e.g. 404 or 500). By
     * default this is <b>false</b>; an IOException is thrown if an error is encountered. If set to <b>true</b>, the
     * response is populated with the error body, and the status message will reflect the error.
     *
     * @param ignoreHttpErrors - false (default) if HTTP errors should be ignored.
     * @return this Connection, for chaining
     */
    public Connection ignoreHttpErrors(boolean ignoreHttpErrors);

    /**
     * Configures the connection to ignore blank parameters when execute.
     *
     * @param ignoreBlankParameters true (default) if blank parameters should be ignored
     * @return this Connection, for chaining
     */
    public Connection ignoreBlankParameters(boolean ignoreBlankParameters);

    /**
     * Disable/enable auto redirect when status code at 300 ~ 400.
     * @param followRedirects if should follow redirect. <b>true</b> by default.
     * @return this Connection, for chaining
     */
    public Connection followRedirects(boolean followRedirects);
    /**
     * Disable/enable TSL certificates validation for HTTPS requests.
     * <p>
     * By default this is <b>true</b>; all
     * connections over HTTPS perform normal validation of certificates, and will abort requests if the provided
     * certificate does not validate.
     * </p>
     * <p>
     * Some servers use expired, self-generated certificates; or your JDK may not
     * support SNI hosts. In which case, you may want to enable this setting.
     * </p>
     * <p>
     * <b>Be careful</b> and understand why you need to disable these validations.
     * </p>
     *
     * @param value if should validate TSL (SSL) certificates. <b>true</b> by default.
     * @return this Connection, for chaining
     */
    Connection validateTLSCertificates(boolean value);

    /**
     * Add a request data parameter. Request parameters are sent in the request query string for GETs, and in the
     * request body for POSTs. A request may have multiple values of the same name.
     *
     * @param key data key
     * @param value data value
     * @return this Connection, for chaining
     */
    public Connection data(String key, Object value);

    /**
     * Add an input stream as a request data paramater. For GETs, has no effect, but for POSTS this will upload the
     * input stream.
     *
     * <p>
     * This will use <strong><code>multipart/form-data</code></strong> for Content-Type header, what ever to {@link #contentType(java.lang.String) } setting will be <strong>ignored</strong>.</p>
     *
     * @param key data key (form item name)
     * @param filename the name of the file to present to the remove server. Typically just the name, not path,
     * component.
     * @param inputStream the input stream to upload, that you probably obtained from a {@link java.io.FileInputStream}.
     * You must close the InputStream in a {@code finally} block.
     * @return this Connections, for chaining
     */
    public Connection data(String key, String filename, InputStream inputStream);

    /**
     * Adds all of the supplied data to the request data parameters
     *
     * @param data collection of data parameters
     * @return this Connection, for chaining
     */
    public Connection data(Collection<KeyVal> data);

    /**
     * Adds all of the supplied data to the request data parameters
     *
     * @param data map of data parameters
     * @return this Connection, for chaining
     */
    public Connection data(Map<String, String> data);

    /**
     * Add a number of request data parameters. Multiple parameters may be set at once, e.g.: <code>.data("name",
     * "jsoup", "language", "Java", "language", "English");</code> creates a query string like:
     * <code>{@literal ?name=jsoup&language=Java&language=English}</code>
     *
     * @param keyvals a set of key value pairs.
     * @return this Connection, for chaining
     */
    public Connection data(String... keyvals);

    /**
     * Set a data parameter signer for this connection.
     *
     * @param signer the signer algorithm
     */
    public Connection signer(Signer signer);

    /**
     * Set a request header.
     *
     * @param name header name
     * @param value header value
     * @return this Connection, for chaining
     * @see org.jsoup.Connection.Request#headers()
     */
    public Connection header(String name, String value);

    /**
     * Set a Content-Type request header. If the contentType end with charset information (like text/xml;charset=UTF-8), this method will call {@link #charset(java.lang.String) } to do that.
     *
     * @param contentType value of Content-Type
     * @return this Connection, for chaining
     */
    public Connection contentType(String contentType);

    /**
     * Set a flag that determine whether write data to output only one time when has body output.
     * Default is true.
     *
     * @param doOutputOnlyOnce true for write data to output only one time; false for no limit - that say will write to network per datas count times.
     * @return this Connection, for chaining
     */
    public Connection doOutputOnlyOnce(boolean doOutputOnlyOnce);

    /**
     * Set a Accept request header.
     *
     * @param accept value of Accept
     * @return this Connection, for chaining
     */
    public Connection accept(String accept);

    /**
     * Set a cookie to be sent in the request.
     *
     * @param name name of cookie
     * @param value value of cookie
     * @return this Connection, for chaining
     */
    public Connection cookie(String name, String value);

    /**
     * Adds each of the supplied cookies to the request.
     *
     * @param cookies map of cookie name {@literal ->} value pairs
     * @return this Connection, for chaining
     */
    public Connection cookies(Map<String, String> cookies);

    /**
     * Sets the default post data character set. Default is {@code UTF-8}.
     *
     * @param charset character set to encode post data
     * @return this Connection, for chaining
     */
    public Connection charset(String charset);

    /**
     * A flag that tell the HttpConnection do not really send the request. Default is {@code false}.
     *
     * <p>
     * It's useful in debug mode: just intrests in debug logs.</p>
     *
     * @return true--will not really send the request.
     */
    public Connection test(boolean test);

    /**
     * Execute the request as a GET, and get the response text.
     *
     * @return the response
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public String get() throws IOException;

    /**
     * Execute the request as a POST, and post the body. All other parameters will append to url.
     *
     * @param body the body to post instead of data parameters, null means use data parameters
     * @return the response
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public String post() throws IOException;

    /**
     * Execute the request as a POST, and post the body. All other parameters will append to url.
     *
     * @param body the body to post instead of data parameters, null means use data parameters
     * @return the response
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public String post(String body) throws IOException;

    /**
     * Execute the request as a GET, and get the callback returns.
     *
     * @return the callback returns
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public <T> T get(ResponseCallable<T> callable) throws IOException;

    /**
     * Execute the request as a POST, and post the body. All other parameters will append to url.
     *
     * @param body the body to post instead of data parameters, null means use data parameters
     * @return the callback returns
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public <T> T post(ResponseCallable<T> callable) throws IOException;

    /**
     * Execute the request as a POST, and post the body. All other parameters will append to url.
     *
     * @param body the body to post instead of data parameters, null means use data parameters
     * @return the callback returns
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public <T> T post(String body, ResponseCallable<T> callable) throws IOException;

    /**
     * Execute the request.
     *
     * @return a response object
     * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
     * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
     * @throws java.net.SocketTimeoutException if the connection times out
     * @throws IOException on error
     */
    public Response execute() throws IOException;

    /**
     * Execute the request. And take a callback which intrest in Response InputStream;
     *
     * @param callable A Callable will be executed during response body InputStream is open
     * @return A value will be return after response close. It simply equals to callable's returning back.
     */
    public <T> T execute(ResponseCallable<T> callable) throws IOException;

    /**
     * Get the request object associated with this connection
     *
     * @return request
     */
    public Request request();

    /**
     * Set the connection's request
     *
     * @param request new request object
     * @return this Connection, for chaining
     */
    public Connection request(Request request);

    /**
     * Get the response, once the request has been executed
     *
     * @return response
     */
    public Response response();

    /**
     * Set the connection's response
     *
     * @param response new response
     * @return this Connection, for chaining
     */
    public Connection response(Response response);

    /**
     * Common methods for Requests and Responses
     *
     * @param <T> Type of Base, either Request or Response
     */
    interface Base<T extends Base> {

        /**
         * Get the URL
         *
         * @return URL
         */
        public URL url();

        /**
         * Set the URL
         *
         * @param url new URL
         * @return this, for chaining
         */
        public T url(URL url);

        /**
         * Get the request method
         *
         * @return method
         */
        public Method method();

        /**
         * Set the request method
         *
         * @param method new method
         * @return this, for chaining
         */
        public T method(Method method);

        /**
         * Get the value of a header. This is a simplified header model, where a header may only have one value.
         * <p>
         * Header names are case insensitive.
         * </p>
         *
         * @param name name of header (case insensitive)
         * @return value of header, or null if not set.
         * @see #hasHeader(String)
         * @see #cookie(String)
         */
        public String header(String name);

        /**
         * Set a header. This method will overwrite any existing header with the same case insensitive name.
         *
         * @param name Name of header
         * @param value Value of header
         * @return this, for chaining
         */
        public T header(String name, String value);

        /**
         * Check if a header is present
         *
         * @param name name of header (case insensitive)
         * @return if the header is present in this request/response
         */
        public boolean hasHeader(String name);

        /**
         * Check if a header is present, with the given value
         *
         * @param name header name (case insensitive)
         * @param value value (case insensitive)
         * @return if the header and value pair are set in this req/res
         */
        public boolean hasHeaderWithValue(String name, String value);

        /**
         * Remove a header by name
         *
         * @param name name of header to remove (case insensitive)
         * @return this, for chaining
         */
        public T removeHeader(String name);

        /**
         * Retrieve all of the request/response headers as a map
         *
         * @return headers
         */
        public Map<String, String> headers();

        /**
         * Get a cookie value by name from this request/response.
         * <p>
         * Response objects have a simplified cookie model. Each cookie set in the response is added to the response
         * object's cookie key=value map. The cookie's path, domain, and expiry date are ignored.
         * </p>
         *
         * @param name name of cookie to retrieve.
         * @return value of cookie, or null if not set
         */
        public String cookie(String name);

        /**
         * Set a cookie in this request/response.
         *
         * @param name name of cookie
         * @param value value of cookie
         * @return this, for chaining
         */
        public T cookie(String name, String value);

        /**
         * Check if a cookie is present
         *
         * @param name name of cookie
         * @return if the cookie is present in this request/response
         */
        public boolean hasCookie(String name);

        /**
         * Remove a cookie by name
         *
         * @param name name of cookie to remove
         * @return this, for chaining
         */
        public T removeCookie(String name);

        /**
         * Retrieve all of the request/response cookies as a map
         *
         * @return cookies
         */
        public Map<String, String> cookies();
    }

    /**
     * Represents a HTTP request.
     */
    public interface Request extends Base<Request> {

        /**
         * Get the request connectTimeout, in milliseconds. Default is 20000.
         *
         * @return the connectTimeout in milliseconds.
         */
        public int connectTimeout();

        /**
         * Get the connection readTimeout, in milliseconds. Default is 40000.
         *
         * @return the readTimeout in milliseconds.
         */
        public int readTimeout();

        /**
         * Update the request connectTimeout. Default is 20000.
         *
         * @param millis connectTimeout, in milliseconds
         * @return this Request, for chaining
         */
        public Request connectTimeout(int millis);

        /**
         * Update the request readTimeout. Default is 40000.
         *
         * @param millis readTimeout, in milliseconds
         * @return this Request, for chaining
         */
        public Request readTimeout(int millis);

        /**
         * Get the maximum body size, in bytes.
         *
         * @return the maximum body size, in bytes.
         */
        public int maxBodySize();

        /**
         * Update the maximum body size, in bytes.
         *
         * @param bytes maximum body size, in bytes.
         * @return this Request, for chaining
         */
        public Request maxBodySize(int bytes);

        /**
         * Get the current followRedirects configuration.
         *
         * @return true if followRedirects is enabled.
         */
        public boolean followRedirects();

        /**
         * Configures the request to (not) follow server redirects. By default this is <b>true</b>.
         *
         * @param followRedirects true if server redirects should be followed.
         * @return this Request, for chaining
         */
        public Request followRedirects(boolean followRedirects);

        /**
         * Get the current ignoreHttpErrors configuration.
         *
         * @return true if errors will be ignored; false (default) if HTTP errors will cause an IOException to be
         * thrown.
         */
        public boolean ignoreHttpErrors();

        /**
         * Configures the request to ignore HTTP errors in the response.
         *
         * @param ignoreHttpErrors set to true to ignore HTTP errors.
         * @return this Request, for chaining
         */
        public Request ignoreHttpErrors(boolean ignoreHttpErrors);

        /**
         * Get the current ignoreBlankParameters configuration.
         *
         * @return true (default) if blank parameters will be ignored
         */
        public boolean ignoreBlankParameters();

        /**
         * Configures the request to ignore blank parameters when execute.
         *
         * @param ignoreBlankParameters true (default) if blank parameters should be ignored
         * @return this Connection, for chaining
         */
        public Request ignoreBlankParameters(boolean ignoreBlankParameters);

        /**
         * Get the current state of TLS (SSL) certificate validation.
         *
         * @return true if TLS cert validation enabled
         */
        boolean validateTLSCertificates();

        /**
         * Set TLS certificate validation.
         *
         * @param value set false to ignore TLS (SSL) certificates
         */
        void validateTLSCertificates(boolean value);

        /**
         * Add a data parameter to the request
         *
         * @param keyval data to add.
         * @return this Request, for chaining
         */
        public Request data(KeyVal keyval);

        /**
         * Get all of the request's data parameters
         *
         * @return collection of keyvals
         */
        public Collection<KeyVal> data();

        /**
         * Set the http request body. This actual body is different from data parameter.
         *
         * @param body body to set
         */
        public void body(String body);

        /**
         * Get the http request body. This actual body is different from data parameter.
         *
         * @return body string
         */
        public String body();

        /**
         * Determine whether the actual request body is set or not. Actual body does not include data parameters,
         * so if an actual body been set the URL must contains data parameters if presented.
         *
         * @return true if the request body is not null
         */
        public boolean hasActualBody();

        /**
         * Sets the post data character set for x-www-form-urlencoded post data
         *
         * @param charset character set to encode post data
         * @return this Request, for chaining
         */
        public Request charset(String charset);

        /**
         * Gets the post data character set for x-www-form-urlencoded post data
         *
         * @return character set to encode post data
         */
        public String charset();

        /**
         * Set a data parameter signer for this request.
         *
         * @param signer the signer algorithm
         */
        public void signer(Signer signer);

        /**
         * Get the data parameter signer for this request.
         *
         * @return the signer algorithm
         */
        public Signer signer();

        /**
         * 连接超时的最大重试次数. 不包括{@code Read time out}.
         *
         * @return 连接超时的最大重试次数
         */
        public int maxRetryTimes();

        /**
         * 设置连接超时的最大重试次数. {@code Read time out}不会重试。
         *
         * @param times 最大重试次数
         */
        public void maxRetryTimes(int times);

        /**
         * A flag that determine whether write data to output only one time when has body output.
         * Default is true.
         *
         * @return true for write data to output only one time; false for no limit - that say will write to network per datas count times.
         */
        public boolean doOutputOnlyOnce();

        /**
         * Set a flag that determine whether write data to output only one time when has body output.
         * Default is true.
         *
         * @param doOutputOnlyOnce true for write data to output only one time; false for no limit - that say will write to network per datas count times.
         */
        public void doOutputOnlyOnce(boolean doOutputOnlyOnce);

        /**
         * A flag that tell the HttpConnection do not really send the request. Default is {@code false}.
         *
         * <p>
         * It's useful in debug mode: just intrests in debug logs.</p>
         *
         * @return true--will not really send the request.
         */
        public boolean test();

        /**
         * A flag that tell the HttpConnection do not really send the request. Default is {@code false}.
         *
         * <p>
         * It's useful in debug mode: just intrests in debug logs.</p>
         *
         * @param test true--will not really send the request.
         */
        public void test(boolean test);

    }

    /**
     * Represents a HTTP response.
     */
    public interface Response<C> extends Base<Response<C>> {

        /**
         * Get the status code of the response.
         *
         * @return status code
         */
        public int statusCode();

        /**
         * Get the status message of the response.
         *
         * @return status message
         */
        public String statusMessage();

        /**
         * Get the character set name of the response.
         *
         * @return character set name
         */
        public String charset();

        /**
         * Get the response content type (e.g. "text/html");
         *
         * @return the response content type
         */
        public String contentType();

        /**
         * Get the body of the response as a plain string.
         *
         * @return body
         */
        public String body();

        /**
         * Get the callback retruns.
         *
         * @return callback retruns
         */
        public C callback();

        /**
         * Get the body of the response as an array of bytes.
         *
         * @return body bytes
         */
        public byte[] bodyAsBytes();
    }

    /**
     * A Key Value tuple.
     */
    public interface KeyVal {

        /**
         * Update the key of a keyval
         *
         * @param key new key
         * @return this KeyVal, for chaining
         */
        public KeyVal key(String key);

        /**
         * Get the key of a keyval
         *
         * @return the key
         */
        public String key();

        /**
         * Update the value of a keyval
         *
         * @param value the new value
         * @return this KeyVal, for chaining
         */
        public KeyVal value(String value);

        /**
         * Get the value of a keyval
         *
         * @return the value
         */
        public String value();

        /**
         * Add or update an input stream to this keyVal
         *
         * @param inputStream new input stream
         * @return this KeyVal, for chaining
         */
        public KeyVal inputStream(InputStream inputStream);

        /**
         * Get the input stream associated with this keyval, if any
         *
         * @return input stream if set, or null
         */
        public InputStream inputStream();

        /**
         * Does this keyval have an input stream?
         *
         * @return true if this keyval does indeed have an input stream
         */
        public boolean hasInputStream();
    }

    /**
     * A Callable will be executed during response body InputStream is open.
     */
    public interface ResponseCallable<T> {

        /**
         * This method will be executed during response body InputStream is open.
         *
         * @param inputStream Response InputStream
         * @return anything
         */
        public T withBodyStream(InputStream inputStream) throws IOException;
    }
}
